Требования: закрыть все приложения от прямого доступа за reverse proxy сервисом в целях безопасности и возможности иметь единую точку входа, что позволит конфигурировать права доступа в едином механизме без необходимости внесения изменений в каждый сервис отдельно. Также это решение покрывает требования от ИБ

Основная задача прокси в том, что он должен исходя из своей конфигурации позволять обращаться к сервисам как с токеном авторизации, так и без него (для возможности открыть страницу авторизации). Будет закрыт прямой доступ ко всем существующим микросервисам и все запросы извне будут проходить через proxy-gateway. После обсуждения с Сергеем Жемотелем принято решение что закрытие всех микросервисов будет произведено путем удаления их ingress, в итоге в кубере будет единственный ingress - proxy-gateway.
Микросервис proxy-gateway разрабатывается на SpringFramework с использованием spring-cloud-gateway. Это известный и хорошо работающий reverse proxy сервис. Обсуждалось также использовать service discovery (eureka), убрать все ingress из кубера, настроить все сервисы на регистрацию в service discovery, тогда балансировкой будет полноценно заниматься zuul proxy, но необходимости в этом нет. Поэтому будем работать без service discovery, балансировка и circuit breaker останется на плечах Service кубера.

Полезные для нас возможности spring-cloud-gateway:

  1. proxy- gateway в в этом случае будет написан на java и не будет необходимости писать новую логику для получения ролей токена, сейчас у нас есть библиотека от систематики - secure-configuration, которая всем этим занимается и имеет возможность подключения к spring приложению, в нее уже внесены доработки для поддержки webflux (требуется в spring-cloud-gateway).
  2. мы можем строить таблицу маршрутизации по любым доступным правилам - хост, path, хедеры, контент запроса. Так можно будет держать один proxy-gateway который будет работать например и с uat и с prod и с dev окружением, исходя из хоста запроса можно направлять траффик на нужный стенд.
  3. имеется возможность проксировать ws соединения, что будет необходимо для работы push уведомлениями в браузере.


Схема работы:

запросы без токена
запросы с токеном
запрос таблицы маршрутизации
proxy-gateway
client
reverse proxy для запросов, маршрутизация согласно выставленным параметрам

внешняя сеть
внутренняя сеть
sso-service
service-registry
document-registry
и все остальные сервисы
запросы в пределах внутренней сети, не требующие авторизации
Services кубера
Pods кубера
service-registry
registry-data
document-registry
и другие
microws
front-end запросы
прямые запросы с токеном авторизации по имени сервиса, например к mz-history
Postrgres
здесь хранится таблица маршрутизации
endpoint-access-info
Кафка, при первом старте


Ingress будет иметь только proxy-gateway, а все наши текущие сервисы будут доступны только через Service, таким образом можно будет убрать у них авторизацию т.к. доступ к ним будет возможен только через proxy-gateway который и будет проверять токены и роли пользователей. Внутри сети микросервисы должны работать через Service.

proxy- gateway требуется информация, по которой он может определять может ли он одобрить запрос к конкретному ендпоинту с той ролью, которая пришла в токене от клиента в виде "/path" - ["роли"]. Таблица маршрутизации хранится в базе Postgres - enpoints_access_info. Proxy-gateway получает содержимое таблицы маршрутов при запуске и далее каждые 10 минут (в текущей реализации пока так).

При таком подходе прокси будет иметь на руках информацию о доступах к ендпоинтам по ролям, далее нужно реализовать механизм актуализации этих данных, например при появлении новых ендпоинтов или при создании нового сервиса необходимо сразу же доставить эту информацию до прокси. И здесь проблема в человеческом факторе - сложно будет избежать опечаток или того что кто-то вообще забудет добавить эту информацию в базу. Поэтому сделано следующее: разработана библиотека pgs-proxy-library, которая содержит аннотацию по аналогии с аннотацией Secured будет принимать набор ролей в параметре, этой аннотацией необходимо будет разметить все ендпоинты сервиса, к которым нужен доступ из прокси, при старте приложения библиотека собирает всю информацию и отправляет ее в enpoints-access-info-service который будет сохранять ее в базе enpoints_access_info. Таким образом имеется два варианта заполнения таблицы маршрутизации:

  1. с помощью библиотеки pgs-proxy-library (micronaut 3, java 17). Полностью автоматический способ.
  2. в ручную заполнять таблицу маршрутизации.

Формат таблицы access_info:

имя поля
формат
описание
пример
iduuidprimary keyaefe7255-6111-4365-886a-e84d805d7b05
app_namevarchar(255)Имя приложения, должно быть равно имени его Service кубераmz-history-2
pkgvarchar(500)Абсолютный путь к java классу с контроллером, поле для информации, не участвует в логике.

sx.microservices.mz.history.controller.RequestController

methodvarchar(255)метод запроса - GET, POST и т.п.GET
ingress_pathvarchar(500)Базовый путь к приложению, то же самое что и сейчас у него в пути ingress./mz/history
pathvarchar(500)Путь к ендпоинту внутри приложения, например "/requests/example". Поддерживается wildcard, пример - "/request/*" - будет принимать любое значение на месте * Еще пример "/request/*/*"/request/example
rulesjsonbJson с полями perms и roles, где perms - массив permissions, roles - массив ролей. Можно указать либо что-то одно либо ничего вообще.{"perms": null, "roles": null} - означает что для доступа достаточно лишь валидного токена, без разницы какие у него роли.
manualbooleanПоле для определения в ручную заполнен этот маршрут запросом в базу или автоматически сервисом enpoints-access-info-servicefalse
modifiedtimestampДата редактирования1682606850000
modified_byvarchar(255)Источник редактированияManual или 

AccessInfoService


Принцип работы

При запуске proxy-gateway (далее - прокси) загружает таблицу маршрутизации из postgres (далее он ее актуализирует каждые 10 минут) и кладет к себе в кеш. При поступлении запроса срабатывает фильтр авторизации - из хедера Authorization либо из Cookie берется токен авторизации и производится проверка токена и извлечение его ролей из sso-service - это все стандартный механизм из secure-configuration, который сейчас используется на ПГС во всех сервисах где есть авторизация, затем он ищет среди ingress_path подходящий сервис для проксирования запроса, далее он определяет по path и method какие необходимы роли для доступа и если пришедший токен удовлетворяет условиям обращения к ендпоинту - проксирует запрос, иначе - выдает ошибку 401. Если не найден подходящий маршрут - возвращается ошибка 404. В прокси предусмотрены конфиги для безусловного доступа к ендпоинтам в не зависимости от наличия токена в запросе - whitelist paths, это было сделано строго для dev и uat площадок, сейчас там содержатся пути к swagger-ui и его составляющим, на проде не рекомендуется делать также. Для прокси дополнен параметр trustedIssuers - в него добавлено поле internalUrl - внутренний адрес sso-service (http://sso-service) для того, чтобы не ходить в sso (за проверкой токена) через внешнюю сеть (по факту через самого себя).

Параметры конфигурации
proxy_gateway:
  # для прода указать "prod"
  spring_profile: dev
  microws_uri: http://microws # внутренний путь к microws, будет нужен если будем прятать microws за прокси
  sso_service_uri: http://sso-service # внутренний адрес sso-service, нужен для проксирования запросов к sso-service
  no_roles_access_allowed: true # если true - то позволять обращаться к ендпоинтам у которых в таблице маршрутизации не заполнено поле rules - по факту это доступ к ендпоинту только с валидным токеном без проверки какие в нем есть роли
  # для прода white_list_paths оставить пустым, этот параметр позволяет заходить по перечисленным в нем путям без авторизации
  white_list_paths: /swagger-ui,/api,/res/swagger-ui-bundle.js,/res/swagger-ui-standalone-preset.js,/res/swagger-ui.css,/res/favicon-32x32.png,/res/favicon-16x16.png,/swagger/swagger.yml,/readiness,/liveness,/health
  max_request_size: 100MB


Библиотека pgs-proxy-library на старте приложения находит все контроллеры с аннотацией PgsAccessRules, принимает из нее роли и permissions, формирует запись вида Method, Path, rules и отправляет через кафку в сервис endpoints-access-info, который сохраняет полную информацию о маршрутах в базу postgres актуализируя записи (если для этого сервиса записи уже есть - он их пересоздаст, таким образом поддерживается удаление и обновление ендпоинтов в таблице маршрутизации), при этом если у существующей записи стоит признак manual = true - то запись не будет обновлена либо удалена, это сделано для того, чтобы иметь возможность в ручную изменять записи и чтобы они после этого не перезаписывались автоматически.


Этапы реализации:


Примеры скриптов в таблицу access_info БД endpoints-access-info

Примеры указаны без учёта требования отдела DBA по оформлению скриптов для релиза.

Полностью одинаковых (дублей) записей в таблице access_info делать не нужно.

Добавление новой записи:

В первую очередь рекомендуется проверить есть ли уже такая запись, чтобы не плодить дубли. Если такая запись уже есть - проверить актуальные ли права доступа в ней.

Скрипт добавления записи
INSERT INTO access_info(app_name,pkg,method,ingress_path,path,rules,manual) VALUES
('ervu-application-gateway','Manual','Get','/service/ervu-application-gateway','/applicationInfo/getWithActive/*','{"perms": [], "roles": []}','true')

Также есть примеры в комментарии к задачам Невозможно найти сервер Jira для этого макроса. Причиной может быть конфигурация ссылки на приложение. и Невозможно найти сервер Jira для этого макроса. Причиной может быть конфигурация ссылки на приложение.

где

app_name - имя приложения, должен быть равен строго имени сущности Service кубера, по нему будет строится путь к сервису, например если app_name = cnsi-service, то путь будет начинаться с http://cnsi-service

pkg - имя класса контроллера, необязательно, делалось для отладок, сейчас особо не используется
method - тип rest метода (get, post, path, put ...) регистронезависимый

ingress_path - внешний путь к сервису, сюда нужно указать то, что было указано в ingress сервиса, например /service/cnsi/service

path - путь к ендпоинту, относительно ingress, например если app_name = cnsi-service, ingress_path = /service/cnsi/service, path = /classifiers/reload - то полный путь будет сформирован так: http://cnsi-service/service/cnsi/service/classifiers/reload

rules - jsonb с правами доступа, внутри объекта должны лежать roles и perms, где roles - требуемые для доступа роли, perms - требуемые permissions. Необязательное поле, если не заполнено - то для доступа к ендпоинту потребуется только валидный токен

manual - если эта запись в таблицу добавляется в ручную - значение этого поля должно быть строго равно true


Примечение:
Изначально обсуждалось что мы не будем использовать permissions, только roles. Желательно этого придерживаться, в текущих записях все таки где-то используем permissions.


Если есть возможность внедрить в сервис библиотеку pgs-proxy-library (http://nexus.gosuslugi.local/#nexus-search;quick~pgs-proxy-library)   - то лучше использовать ее, она сама инициирует заполнение таблицы путей (через сообщения кафки в сервис endpoints-access-info) и будет их актуализировать. Основная версия для java17 и micronaut3, есть версии для java11 и micronaut2. Для работы библиотеки необходимо:

1. Обеспечить права на запись в топик pgs.proxy.endpoints.access.info

2. В application.yml добавить:
pgs-proxy:
  enabled: true # на стендах пгс прокси не используется, поэтому там нужно выставить false
  ingress-path: /service/ervu-object-diff-calc # сюда нужно указать базовый путь к сервису, если был ingress - взять путь из него
  packages: rtl.pgs.ervu.objectdiffcalc # путь в корневому package приложения
  kafka:
    endpoints-access-info-topic: pgs.proxy.endpoints.access.info # имя топика, в который нужно отправлять инфу об эндпоинтах сервиса, не менять

3. Убрать ingress из хелмов

4. Все методы, к которым необходим внешний доступ, обозначить аннотацией @PgsAccessRules, в параметрах этой аннотации указать roles и perms, либо не указывать (если нужен доступ просто по валидному токену)


После добавления записи в таблицу (либо после деплоя сервиса с включенной библиотекой pgs-proxy-library) сервис прокси подхватит изменения в течении 10 минут.


Обновление записи в таблице:

Обновление по id, задание прав доступа:

Скрипт обновления записи
UPDATE access_info
SET rules = '{"roles": ["Администратор облака"], "perms": "read_user"}'
WHERE id = 2000000000001

Удаление записи в таблице:

Для удаления лучше всего использовать id чтобы не удалить лишнего

Скрипт удаления записи
DELETE
FROM access_info
WHERE id = <ID>
Написать комментарий...